/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 */
package org.forgerock.opendj.config.dsconfig;

import static com.forgerock.opendj.cli.ReturnCode.*;
import static com.forgerock.opendj.dsconfig.DsconfigMessages.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.opendj.config.AbsoluteInheritedDefaultBehaviorProvider;
import org.forgerock.opendj.config.AbstractManagedObjectDefinition;
import org.forgerock.opendj.config.AggregationPropertyDefinition;
import org.forgerock.opendj.config.AliasDefaultBehaviorProvider;
import org.forgerock.opendj.config.BooleanPropertyDefinition;
import org.forgerock.opendj.config.Configuration;
import org.forgerock.opendj.config.ConfigurationClient;
import org.forgerock.opendj.config.DefaultBehaviorProviderVisitor;
import org.forgerock.opendj.config.DefinedDefaultBehaviorProvider;
import org.forgerock.opendj.config.DefinitionDecodingException;
import org.forgerock.opendj.config.EnumPropertyDefinition;
import org.forgerock.opendj.config.InstantiableRelationDefinition;
import org.forgerock.opendj.config.ManagedObjectDefinition;
import org.forgerock.opendj.config.ManagedObjectNotFoundException;
import org.forgerock.opendj.config.ManagedObjectPath;
import org.forgerock.opendj.config.PropertyDefinition;
import org.forgerock.opendj.config.PropertyDefinitionUsageBuilder;
import org.forgerock.opendj.config.PropertyDefinitionVisitor;
import org.forgerock.opendj.config.PropertyException;
import org.forgerock.opendj.config.PropertyOption;
import org.forgerock.opendj.config.RelativeInheritedDefaultBehaviorProvider;
import org.forgerock.opendj.config.UndefinedDefaultBehaviorProvider;
import org.forgerock.opendj.config.client.ManagedObject;
import org.forgerock.opendj.config.client.ManagedObjectDecodingException;
import org.forgerock.opendj.config.client.ManagementContext;
import org.forgerock.opendj.ldap.AuthorizationException;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.util.Reject;

import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.ConsoleApplication;
import com.forgerock.opendj.cli.HelpCallback;
import com.forgerock.opendj.cli.Menu;
import com.forgerock.opendj.cli.MenuBuilder;
import com.forgerock.opendj.cli.MenuCallback;
import com.forgerock.opendj.cli.MenuResult;
import com.forgerock.opendj.cli.ReturnCode;
import com.forgerock.opendj.cli.TableBuilder;
import com.forgerock.opendj.cli.TextTablePrinter;

import static org.forgerock.opendj.config.dsconfig.DSConfig.*;

/** Common methods used for interactively editing properties. */
final class PropertyValueEditor {

    /**
     * A menu call-back which can be used to dynamically create new components when configuring aggregation based
     * properties.
     */
    private final class CreateComponentCallback<C extends ConfigurationClient, S extends Configuration> implements
            MenuCallback<String> {

        /** The aggregation property definition. */
        private final AggregationPropertyDefinition<C, S> pd;

        /** Creates a new component create call-back for the provided aggregation property definition. */
        private CreateComponentCallback(AggregationPropertyDefinition<C, S> pd) {
            this.pd = pd;
        }

        @Override
        public MenuResult<String> invoke(ConsoleApplication app) throws ClientException {
            try {
                // First get the parent managed object.
                InstantiableRelationDefinition<?, ?> rd = pd.getRelationDefinition();
                ManagedObjectPath<?, ?> path = pd.getParentPath();
                LocalizableMessage ufn = rd.getUserFriendlyName();

                ManagedObject<?> parent;
                try {
                    parent = context.getManagedObject(path);
                } catch (AuthorizationException e) {
                    LocalizableMessage msg = ERR_DSCFG_ERROR_CREATE_AUTHZ.get(ufn);
                    throw new ClientException(ReturnCode.INSUFFICIENT_ACCESS_RIGHTS, msg);
                } catch (DefinitionDecodingException e) {
                    LocalizableMessage pufn = path.getManagedObjectDefinition().getUserFriendlyName();
                    LocalizableMessage msg = ERR_DSCFG_ERROR_GET_PARENT_DDE.get(pufn, pufn, pufn);
                    throw new ClientException(ReturnCode.OTHER, msg);
                } catch (ManagedObjectDecodingException e) {
                    LocalizableMessage pufn = path.getManagedObjectDefinition().getUserFriendlyName();
                    LocalizableMessage msg = ERR_DSCFG_ERROR_GET_PARENT_MODE.get(pufn);
                    throw new ClientException(ReturnCode.OTHER, msg, e);
                } catch (LdapException e) {
                    LocalizableMessage msg = ERR_DSCFG_ERROR_CREATE_CE.get(ufn, e.getMessage());
                    throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN, msg);
                } catch (ManagedObjectNotFoundException e) {
                    LocalizableMessage pufn = path.getManagedObjectDefinition().getUserFriendlyName();
                    LocalizableMessage msg = ERR_DSCFG_ERROR_GET_PARENT_MONFE.get(pufn);
                    return interactivePrintOrThrowError(app, msg, NO_SUCH_OBJECT);
                }

                // Now let the user create the child component.
                app.println();
                app.println();
                return CreateSubCommandHandler.createManagedObject(app, context, parent, rd);
            } catch (ClientException e) {
                // FIXME: should really do something better with the exception
                // handling here. For example, if a authz or communications
                // exception occurs then the application should exit.
                app.errPrintln();
                app.errPrintln(e.getMessageObject());
                app.errPrintln();
                app.pressReturnToContinue();
                return MenuResult.cancel();
            }
        }
    }

    /** A help call-back which displays a description and summary of a component and its properties. */
    private static final class ComponentHelpCallback implements HelpCallback {

        /** The managed object being edited. */
        private final ManagedObject<?> mo;

        /** The properties that can be edited. */
        private final Collection<PropertyDefinition<?>> properties;

        /** Creates a new component helper for the specified property. */
        private ComponentHelpCallback(ManagedObject<?> mo, Collection<PropertyDefinition<?>> c) {
            this.mo = mo;
            this.properties = c;
        }

        @Override
        public void display(ConsoleApplication app) {
            app.println();
            HelpSubCommandHandler.displaySingleComponent(app, mo, properties);
            app.println();
            app.pressReturnToContinue();
        }
    }

    /** A simple interface for querying and retrieving common default behavior properties. */
    private static final class DefaultBehaviorQuery<T> {

        /** The type of default behavior. */
        private enum Type {
            /** Alias default behavior. */
            ALIAS,
            /** Defined default behavior. */
            DEFINED,
            /** Inherited default behavior. */
            INHERITED,
            /** Undefined default behavior. */
            UNDEFINED;
        }

        /**
         * Create a new default behavior query object based on the provied property definition.
         *
         * @param <T>
         *            The type of property definition.
         * @param pd
         *            The property definition.
         * @return The default behavior query object.
         */
        public static <T> DefaultBehaviorQuery<T> query(PropertyDefinition<T> pd) {
            DefaultBehaviorProviderVisitor<T, DefaultBehaviorQuery<T>, PropertyDefinition<T>> visitor
                = new DefaultBehaviorProviderVisitor<T, DefaultBehaviorQuery<T>, PropertyDefinition<T>>() {

                    @Override
                    public DefaultBehaviorQuery<T> visitAbsoluteInherited(AbsoluteInheritedDefaultBehaviorProvider<T> d,
                            PropertyDefinition<T> p) {
                        AbstractManagedObjectDefinition<?, ?> mod = d.getManagedObjectDefinition();
                        String propertyName = d.getPropertyName();
                        PropertyDefinition<?> pd2 = mod.getPropertyDefinition(propertyName);

                        DefaultBehaviorQuery<?> query = query(pd2);
                        return new DefaultBehaviorQuery<>(Type.INHERITED, query.getAliasDescription());
                    }

                    @Override
                    public DefaultBehaviorQuery<T> visitAlias(AliasDefaultBehaviorProvider<T> d,
                            PropertyDefinition<T> p) {
                        return new DefaultBehaviorQuery<>(Type.ALIAS, d.getSynopsis());
                    }

                    @Override
                    public DefaultBehaviorQuery<T> visitDefined(DefinedDefaultBehaviorProvider<T> d,
                            PropertyDefinition<T> p) {
                        return new DefaultBehaviorQuery<>(Type.DEFINED, null);
                    }

                    @Override
                    public DefaultBehaviorQuery<T> visitRelativeInherited(RelativeInheritedDefaultBehaviorProvider<T> d,
                            PropertyDefinition<T> p) {
                        AbstractManagedObjectDefinition<?, ?> mod = d.getManagedObjectDefinition();
                        String propertyName = d.getPropertyName();
                        PropertyDefinition<?> pd2 = mod.getPropertyDefinition(propertyName);

                        DefaultBehaviorQuery<?> query = query(pd2);
                        return new DefaultBehaviorQuery<>(Type.INHERITED, query.getAliasDescription());
                    }

                    @Override
                    public DefaultBehaviorQuery<T> visitUndefined(UndefinedDefaultBehaviorProvider<T> d,
                            PropertyDefinition<T> p) {
                        return new DefaultBehaviorQuery<>(Type.UNDEFINED, null);
                    }
                };

            return pd.getDefaultBehaviorProvider().accept(visitor, pd);
        }

        /** The description of the behavior if it is an alias default behavior. */
        private final LocalizableMessage aliasDescription;

        /** The type of behavior. */
        private final Type type;

        /** Private constructor. */
        private DefaultBehaviorQuery(Type type, LocalizableMessage aliasDescription) {
            this.type = type;
            this.aliasDescription = aliasDescription;
        }

        /**
         * Gets the detailed description of this default behavior if it is an alias default behavior or if it inherits
         * from an alias default behavior.
         *
         * @return Returns the detailed description of this default behavior if it is an alias default behavior or if it
         *         inherits from an alias default behavior, otherwise <code>null</code>.
         */
        public LocalizableMessage getAliasDescription() {
            return aliasDescription;
        }

        /**
         * Determines whether the default behavior is alias.
         *
         * @return Returns <code>true</code> if the default behavior is alias.
         */
        public boolean isAlias() {
            return type == Type.ALIAS;
        }

        /**
         * Determines whether the default behavior is defined.
         *
         * @return Returns <code>true</code> if the default behavior is defined.
         */
        public boolean isDefined() {
            return type == Type.DEFINED;
        }

        /**
         * Determines whether the default behavior is inherited.
         *
         * @return Returns <code>true</code> if the default behavior is inherited.
         */
        public boolean isInherited() {
            return type == Type.INHERITED;
        }

        /**
         * Determines whether the default behavior is undefined.
         *
         * @return Returns <code>true</code> if the default behavior is undefined.
         */
        public boolean isUndefined() {
            return type == Type.UNDEFINED;
        }

    }

    /** A property definition visitor which initializes mandatory properties. */
    private final class MandatoryPropertyInitializer extends PropertyDefinitionVisitor<MenuResult<Void>, Void>
            implements MenuCallback<Void> {

        /** Any exception that was caught during processing. */
        private ClientException e;

        /** The managed object being edited. */
        private final ManagedObject<?> mo;

        /** The property to be edited. */
        private final PropertyDefinition<?> pd;

        /** Creates a new property editor for the specified property. */
        private MandatoryPropertyInitializer(ManagedObject<?> mo, PropertyDefinition<?> pd) {
            this.mo = mo;
            this.pd = pd;
        }

        @Override
        public MenuResult<Void> invoke(ConsoleApplication app) throws ClientException {
            displayPropertyHeader(app, pd);

            MenuResult<Void> result = pd.accept(this, null);

            if (e != null) {
                throw e;
            }
            return result;
        }

        @Override
        public <C extends ConfigurationClient, S extends Configuration> MenuResult<Void> visitAggregation(
                AggregationPropertyDefinition<C, S> d, Void p) {
            MenuBuilder<String> builder = new MenuBuilder<>(app);
            builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD);

            InstantiableRelationDefinition<C, S> rd = d.getRelationDefinition();
            if (d.hasOption(PropertyOption.MULTI_VALUED)) {
                builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_COMPONENT_MULTI.get(rd.getUserFriendlyPluralName(),
                        d.getName()));
                builder.setAllowMultiSelect(true);
            } else {
                builder.setPrompt(
                        INFO_EDITOR_PROMPT_SELECT_COMPONENT_SINGLE.get(rd.getUserFriendlyName(), d.getName()));
            }

            // Create a list of possible names.
            Set<String> values = new TreeSet<>(d);
            ManagedObjectPath<?, ?> path = d.getParentPath();
            try {
                values.addAll(Arrays.asList(context.listManagedObjects(path, rd)));
            } catch (AuthorizationException e) {
                this.e = new ClientException(
                        ReturnCode.CLIENT_SIDE_PARAM_ERROR, LocalizableMessage.raw(e.getMessage()));
                return MenuResult.quit();
            } catch (ManagedObjectNotFoundException e) {
                this.e = new ClientException(ReturnCode.NO_SUCH_OBJECT, e.getMessageObject());
                return MenuResult.cancel();
            } catch (LdapException e) {
                this.e = new ClientException(ReturnCode.APPLICATION_ERROR, LocalizableMessage.raw(e.getMessage()));
                return MenuResult.quit();
            }

            for (String value : values) {
                LocalizableMessage option = getPropertyValues(d, Collections.singleton(value));
                builder.addNumberedOption(option, MenuResult.success(value));
            }
            MenuCallback<String> callback = new CreateComponentCallback<>(d);
            builder.addNumberedOption(
                    INFO_EDITOR_OPTION_CREATE_A_NEW_COMPONENT.get(rd.getUserFriendlyName()), callback);

            builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));
            if (app.isMenuDrivenMode()) {
                builder.addCancelOption(true);
            }
            builder.addQuitOption();

            Menu<String> menu = builder.toMenu();
            try {
                app.println();
                MenuResult<String> result = menu.run();

                if (result.isQuit()) {
                    return MenuResult.quit();
                } else if (result.isCancel()) {
                    return MenuResult.cancel();
                } else {
                    Collection<String> newValues = result.getValues();
                    SortedSet<String> oldValues = new TreeSet<>(mo.getPropertyValues(d));
                    mo.setPropertyValues(d, newValues);
                    isLastChoiceReset = false;
                    registerModification(d, new TreeSet<String>(newValues), oldValues);
                    return MenuResult.success();
                }
            } catch (ClientException e) {
                this.e = e;
                return MenuResult.cancel();
            }
        }

        @Override
        public MenuResult<Void> visitBoolean(BooleanPropertyDefinition d, Void p) {
            MenuBuilder<Boolean> builder = new MenuBuilder<>(app);

            builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUE_SINGLE.get(d.getName()));

            builder.addNumberedOption(INFO_VALUE_TRUE.get(), MenuResult.success(true));
            builder.addNumberedOption(INFO_VALUE_FALSE.get(), MenuResult.success(false));

            builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));
            if (app.isMenuDrivenMode()) {
                builder.addCancelOption(true);
            }
            builder.addQuitOption();

            Menu<Boolean> menu = builder.toMenu();
            try {
                app.println();
                MenuResult<Boolean> result = menu.run();

                if (result.isQuit()) {
                    return MenuResult.quit();
                } else if (result.isCancel()) {
                    return MenuResult.cancel();
                } else {
                    Collection<Boolean> newValues = result.getValues();
                    SortedSet<Boolean> oldValues = new TreeSet<>(mo.getPropertyValues(d));
                    mo.setPropertyValues(d, newValues);
                    isLastChoiceReset = false;
                    registerModification(d, new TreeSet<Boolean>(newValues), oldValues);
                    return MenuResult.success();
                }
            } catch (ClientException e) {
                this.e = e;
                return MenuResult.cancel();
            }
        }

        @Override
        public <E extends Enum<E>> MenuResult<Void> visitEnum(EnumPropertyDefinition<E> d, Void x) {
            MenuBuilder<E> builder = new MenuBuilder<>(app);
            builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD);

            if (d.hasOption(PropertyOption.MULTI_VALUED)) {
                builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUE_MULTI.get(d.getName()));
                builder.setAllowMultiSelect(true);
            } else {
                builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUE_SINGLE.get(d.getName()));
            }

            Set<E> values = new TreeSet<>(d);
            values.addAll(EnumSet.allOf(d.getEnumClass()));
            for (E value : values) {
                LocalizableMessage option = getPropertyValues(d, Collections.singleton(value));
                builder.addNumberedOption(option, MenuResult.success(value));
            }

            builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));
            if (app.isMenuDrivenMode()) {
                builder.addCancelOption(true);
            }
            builder.addQuitOption();

            Menu<E> menu = builder.toMenu();
            try {
                app.println();
                MenuResult<E> result = menu.run();

                if (result.isQuit()) {
                    return MenuResult.quit();
                } else if (result.isCancel()) {
                    return MenuResult.cancel();
                } else {
                    Collection<E> newValues = result.getValues();
                    SortedSet<E> oldValues = new TreeSet<>(mo.getPropertyValues(d));
                    mo.setPropertyValues(d, newValues);
                    isLastChoiceReset = false;
                    registerModification(d, new TreeSet<E>(newValues), oldValues);
                    return MenuResult.success();
                }
            } catch (ClientException e) {
                this.e = e;
                return MenuResult.cancel();
            }
        }

        @Override
        public <T> MenuResult<Void> visitUnknown(PropertyDefinition<T> d, Void p) {
            app.println();
            displayPropertySyntax(app, d);

            // Set the new property value(s).
            try {
                SortedSet<T> values = readPropertyValues(app, mo.getManagedObjectDefinition(), d);
                SortedSet<T> oldValues = new TreeSet<>(mo.getPropertyValues(d));
                mo.setPropertyValues(d, values);
                isLastChoiceReset = false;
                registerModification(d, values, oldValues);
                return MenuResult.success();
            } catch (ClientException e) {
                this.e = e;
                return MenuResult.cancel();
            }
        }

    }

    /** A menu call-back for editing a modifiable multi-valued property. */
    private final class MultiValuedPropertyEditor extends PropertyDefinitionVisitor<MenuResult<Boolean>, Void>
            implements MenuCallback<Boolean> {

        /** Any exception that was caught during processing. */
        private ClientException e;

        /** The managed object being edited. */
        private final ManagedObject<?> mo;

        /** The property to be edited. */
        private final PropertyDefinition<?> pd;

        /** Creates a new property editor for the specified property. */
        private MultiValuedPropertyEditor(ManagedObject<?> mo, PropertyDefinition<?> pd) {
            Reject.ifFalse(pd.hasOption(PropertyOption.MULTI_VALUED));

            this.mo = mo;
            this.pd = pd;
        }

        @Override
        public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
            displayPropertyHeader(app, pd);

            MenuResult<Boolean> result = pd.accept(this, null);
            if (e != null) {
                throw e;
            }
            return result;
        }

        @Override
        public <C extends ConfigurationClient, S extends Configuration> MenuResult<Boolean> visitAggregation(
                final AggregationPropertyDefinition<C, S> d, Void p) {
            final SortedSet<String> defaultValues = mo.getPropertyDefaultValues(d);
            final SortedSet<String> oldValues = mo.getPropertyValues(d);
            final SortedSet<String> currentValues = mo.getPropertyValues(d);
            final InstantiableRelationDefinition<C, S> rd = d.getRelationDefinition();
            final LocalizableMessage ufpn = rd.getUserFriendlyPluralName();

            boolean isFirst = true;
            while (true) {
                if (!isFirst) {
                    app.println();
                    app.println(INFO_EDITOR_HEADING_CONFIGURE_PROPERTY_CONT.get(d.getName()));
                } else {
                    isFirst = false;
                }

                if (currentValues.size() > 1) {
                    app.println();
                    app.println(INFO_EDITOR_HEADING_COMPONENT_SUMMARY.get(d.getName(), ufpn));
                    app.println();
                    displayPropertyValues(app, d, currentValues);
                }

                // Create a list of possible names.
                final Set<String> values = new TreeSet<>(d);
                ManagedObjectPath<?, ?> path = d.getParentPath();
                try {
                    values.addAll(Arrays.asList(context.listManagedObjects(path, rd)));
                } catch (AuthorizationException e) {
                    this.e = new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, LocalizableMessage.raw(e
                            .getMessage()));
                    return MenuResult.quit();
                } catch (ManagedObjectNotFoundException e) {
                    this.e = new ClientException(ReturnCode.NO_SUCH_OBJECT, e.getMessageObject());
                    return MenuResult.cancel();
                } catch (LdapException e) {
                    this.e = new ClientException(ReturnCode.APPLICATION_ERROR, LocalizableMessage.raw(e.getMessage()));
                    return MenuResult.quit();
                }

                // Create the add values call-back.
                MenuCallback<Boolean> addCallback = null;
                values.removeAll(currentValues);
                if (!values.isEmpty()) {
                    addCallback = new MenuCallback<Boolean>() {

                        @Override
                        public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                            MenuBuilder<String> builder = new MenuBuilder<>(app);

                            builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_COMPONENTS_ADD.get(ufpn));
                            builder.setAllowMultiSelect(true);
                            builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD);

                            for (String value : values) {
                                LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value));
                                builder.addNumberedOption(svalue, MenuResult.success(value));
                            }
                            MenuCallback<String> callback = new CreateComponentCallback<>(d);
                            builder.addNumberedOption(
                                    INFO_EDITOR_OPTION_CREATE_A_NEW_COMPONENT.get(rd.getUserFriendlyName()), callback);

                            if (values.size() > 1) {
                                // No point in having this option if there's only one
                                // possible value.
                                builder.addNumberedOption(INFO_EDITOR_OPTION_ADD_ALL_COMPONENTS.get(ufpn),
                                        MenuResult.success(values));
                            }

                            builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));

                            builder.addCancelOption(true);
                            builder.addQuitOption();

                            app.println();
                            app.println();
                            Menu<String> menu = builder.toMenu();
                            MenuResult<String> result = menu.run();

                            if (result.isSuccess()) {
                                // Set the new property value(s).
                                Collection<String> addedValues = result.getValues();
                                currentValues.addAll(addedValues);

                                isLastChoiceReset = false;
                                app.println();
                                app.pressReturnToContinue();
                                return MenuResult.success(false);
                            } else if (result.isCancel()) {
                                app.println();
                                app.pressReturnToContinue();
                                return MenuResult.success(false);
                            } else {
                                return MenuResult.quit();
                            }
                        }

                    };
                }

                // Create the remove values call-back.
                MenuCallback<Boolean> removeCallback = new MenuCallback<Boolean>() {

                    @Override
                    public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                        MenuBuilder<String> builder = new MenuBuilder<>(app);

                        builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_COMPONENTS_REMOVE.get(ufpn));
                        builder.setAllowMultiSelect(true);
                        builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD);

                        for (String value : currentValues) {
                            LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value));
                            builder.addNumberedOption(svalue, MenuResult.success(value));
                        }

                        builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));

                        builder.addCancelOption(true);
                        builder.addQuitOption();

                        app.println();
                        app.println();
                        Menu<String> menu = builder.toMenu();
                        MenuResult<String> result = menu.run();

                        if (result.isSuccess()) {
                            // Set the new property value(s).
                            Collection<String> removedValues = result.getValues();
                            currentValues.removeAll(removedValues);
                            isLastChoiceReset = false;
                            app.println();
                            app.pressReturnToContinue();
                            return MenuResult.success(false);
                        } else if (result.isCancel()) {
                            app.println();
                            app.pressReturnToContinue();
                            return MenuResult.success(false);
                        } else {
                            return MenuResult.quit();
                        }
                    }

                };

                MenuResult<Boolean> result = runMenu(d, app, defaultValues, oldValues, currentValues, addCallback,
                        removeCallback);
                if (!result.isAgain()) {
                    return result;
                }
            }
        }

        @Override
        public <T extends Enum<T>> MenuResult<Boolean> visitEnum(final EnumPropertyDefinition<T> d, Void p) {
            final SortedSet<T> defaultValues = mo.getPropertyDefaultValues(d);
            final SortedSet<T> oldValues = mo.getPropertyValues(d);
            final SortedSet<T> currentValues = mo.getPropertyValues(d);

            boolean isFirst = true;
            while (true) {
                if (!isFirst) {
                    app.println();
                    app.println(INFO_EDITOR_HEADING_CONFIGURE_PROPERTY_CONT.get(d.getName()));
                } else {
                    isFirst = false;
                }

                if (currentValues.size() > 1) {
                    app.println();
                    app.println(INFO_EDITOR_HEADING_VALUES_SUMMARY.get(d.getName()));
                    app.println();
                    displayPropertyValues(app, d, currentValues);
                }

                // Create the add values call-back.
                MenuCallback<Boolean> addCallback = null;

                final EnumSet<T> values = EnumSet.allOf(d.getEnumClass());
                values.removeAll(currentValues);

                if (!values.isEmpty()) {
                    addCallback = new MenuCallback<Boolean>() {

                        @Override
                        public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                            MenuBuilder<T> builder = new MenuBuilder<>(app);

                            builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUES_ADD.get());
                            builder.setAllowMultiSelect(true);
                            builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD);

                            for (T value : values) {
                                LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value));
                                builder.addNumberedOption(svalue, MenuResult.success(value));
                            }

                            if (values.size() > 1) {
                                // No point in having this option if there's only one
                                // possible value.
                                builder.addNumberedOption(INFO_EDITOR_OPTION_ADD_ALL_VALUES.get(),
                                        MenuResult.success(values));
                            }

                            builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));

                            builder.addCancelOption(true);
                            builder.addQuitOption();

                            app.println();
                            app.println();
                            Menu<T> menu = builder.toMenu();
                            MenuResult<T> result = menu.run();

                            if (result.isSuccess()) {

                                // Set the new property value(s).
                                Collection<T> addedValues = result.getValues();
                                currentValues.addAll(addedValues);
                                isLastChoiceReset = false;
                                app.println();
                                app.pressReturnToContinue();
                                return MenuResult.success(false);
                            } else if (result.isCancel()) {
                                app.println();
                                app.pressReturnToContinue();
                                return MenuResult.success(false);
                            } else {
                                return MenuResult.quit();
                            }
                        }

                    };
                }

                // Create the remove values call-back.
                MenuCallback<Boolean> removeCallback = new MenuCallback<Boolean>() {

                    @Override
                    public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                        MenuBuilder<T> builder = new MenuBuilder<>(app);

                        builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUES_REMOVE.get());
                        builder.setAllowMultiSelect(true);
                        builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD);

                        for (T value : currentValues) {
                            LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value));
                            builder.addNumberedOption(svalue, MenuResult.success(value));
                        }

                        builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));

                        builder.addCancelOption(true);
                        builder.addQuitOption();

                        app.println();
                        app.println();
                        Menu<T> menu = builder.toMenu();
                        MenuResult<T> result = menu.run();

                        if (result.isSuccess()) {
                            // Set the new property value(s).
                            Collection<T> removedValues = result.getValues();
                            currentValues.removeAll(removedValues);
                            isLastChoiceReset = false;
                            app.println();
                            app.pressReturnToContinue();
                            return MenuResult.success(false);
                        } else if (result.isCancel()) {
                            app.println();
                            app.pressReturnToContinue();
                            return MenuResult.success(false);
                        } else {
                            return MenuResult.quit();
                        }
                    }

                };

                MenuResult<Boolean> result = runMenu(d, app, defaultValues, oldValues, currentValues, addCallback,
                        removeCallback);
                if (!result.isAgain()) {
                    return result;
                }
            }
        }

        @Override
        public <T> MenuResult<Boolean> visitUnknown(final PropertyDefinition<T> d, Void p) {
            app.println();
            displayPropertySyntax(app, d);

            final SortedSet<T> defaultValues = mo.getPropertyDefaultValues(d);
            final SortedSet<T> oldValues = mo.getPropertyValues(d);
            final SortedSet<T> currentValues = mo.getPropertyValues(d);

            boolean isFirst = true;
            while (true) {
                if (!isFirst) {
                    app.println();
                    app.println(INFO_EDITOR_HEADING_CONFIGURE_PROPERTY_CONT.get(d.getName()));
                } else {
                    isFirst = false;
                }

                if (currentValues.size() > 1) {
                    app.println();
                    app.println(INFO_EDITOR_HEADING_VALUES_SUMMARY.get(d.getName()));
                    app.println();
                    displayPropertyValues(app, d, currentValues);
                }

                // Create the add values call-back.
                MenuCallback<Boolean> addCallback = new MenuCallback<Boolean>() {

                    @Override
                    public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                        app.println();
                        SortedSet<T> previousValues = new TreeSet<>(currentValues);
                        readPropertyValues(app, mo.getManagedObjectDefinition(), d, currentValues);
                        SortedSet<T> addedValues = new TreeSet<>(currentValues);
                        addedValues.removeAll(previousValues);
                        isLastChoiceReset = false;
                        return MenuResult.success(false);
                    }

                };

                // Create the remove values call-back.
                MenuCallback<Boolean> removeCallback = new MenuCallback<Boolean>() {

                    @Override
                    public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                        MenuBuilder<T> builder = new MenuBuilder<>(app);

                        builder.setPrompt(INFO_EDITOR_PROMPT_SELECT_VALUES_REMOVE.get());
                        builder.setAllowMultiSelect(true);
                        builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD);

                        for (T value : currentValues) {
                            LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value));
                            builder.addNumberedOption(svalue, MenuResult.success(value));
                        }

                        builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));

                        builder.addCancelOption(true);
                        builder.addQuitOption();

                        app.println();
                        app.println();
                        Menu<T> menu = builder.toMenu();
                        MenuResult<T> result = menu.run();

                        if (result.isSuccess()) {
                            // Set the new property value(s).
                            Collection<T> removedValues = result.getValues();
                            currentValues.removeAll(removedValues);
                            isLastChoiceReset = false;
                            app.println();
                            app.pressReturnToContinue();
                            return MenuResult.success(false);
                        } else if (result.isCancel()) {
                            app.println();
                            app.pressReturnToContinue();
                            return MenuResult.success(false);
                        } else {
                            return MenuResult.quit();
                        }
                    }

                };

                MenuResult<Boolean> result = runMenu(d, app, defaultValues, oldValues, currentValues, addCallback,
                        removeCallback);
                if (!result.isAgain()) {
                    return result;
                }
            }
        }

        /**
         * Generate an appropriate menu option for a property which asks the user whether they want to keep the
         * property's current settings.
         */
        private <T> LocalizableMessage getKeepDefaultValuesMenuOption(PropertyDefinition<T> pd,
                SortedSet<T> defaultValues, SortedSet<T> oldValues, SortedSet<T> currentValues) {
            DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd);

            boolean isModified = !currentValues.equals(oldValues);
            boolean isDefault = currentValues.equals(defaultValues);

            if (isModified) {
                switch (currentValues.size()) {
                case 0:
                    if (query.isAlias()) {
                        return INFO_EDITOR_OPTION_USE_DEFAULT_ALIAS.get(query.getAliasDescription());
                    } else if (query.isInherited()) {
                        if (query.getAliasDescription() != null) {
                            return INFO_EDITOR_OPTION_USE_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription());
                        } else {
                            return INFO_EDITOR_OPTION_USE_DEFAULT_INHERITED_ALIAS_UNDEFINED.get();
                        }
                    } else {
                        return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get();
                    }
                case 1:
                    LocalizableMessage svalue = getPropertyValues(pd, currentValues);
                    if (isDefault) {
                        if (query.isInherited()) {
                            return INFO_EDITOR_OPTION_USE_INHERITED_DEFAULT_VALUE.get(svalue);
                        } else {
                            return INFO_EDITOR_OPTION_USE_DEFAULT_VALUE.get(svalue);
                        }
                    } else {
                        return INFO_EDITOR_OPTION_USE_VALUE.get(svalue);
                    }
                default:
                    if (isDefault) {
                        if (query.isInherited()) {
                            return INFO_EDITOR_OPTION_USE_INHERITED_DEFAULT_VALUES.get();
                        } else {
                            return INFO_EDITOR_OPTION_USE_DEFAULT_VALUES.get();
                        }
                    } else {
                        return INFO_EDITOR_OPTION_USE_VALUES.get();
                    }
                }
            } else {
                switch (currentValues.size()) {
                case 0:
                    if (query.isAlias()) {
                        return INFO_EDITOR_OPTION_KEEP_DEFAULT_ALIAS.get(query.getAliasDescription());
                    } else if (query.isInherited()) {
                        if (query.getAliasDescription() != null) {
                            return INFO_EDITOR_OPTION_KEEP_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription());
                        } else {
                            return INFO_EDITOR_OPTION_KEEP_DEFAULT_INHERITED_ALIAS_UNDEFINED.get();
                        }
                    } else {
                        return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get();
                    }
                case 1:
                    LocalizableMessage svalue = getPropertyValues(pd, currentValues);
                    if (isDefault) {
                        if (query.isInherited()) {
                            return INFO_EDITOR_OPTION_KEEP_INHERITED_DEFAULT_VALUE.get(svalue);
                        } else {
                            return INFO_EDITOR_OPTION_KEEP_DEFAULT_VALUE.get(svalue);
                        }
                    } else {
                        return INFO_EDITOR_OPTION_KEEP_VALUE.get(svalue);
                    }
                default:
                    if (isDefault) {
                        if (query.isInherited()) {
                            return INFO_EDITOR_OPTION_KEEP_INHERITED_DEFAULT_VALUES.get();
                        } else {
                            return INFO_EDITOR_OPTION_KEEP_DEFAULT_VALUES.get();
                        }
                    } else {
                        return INFO_EDITOR_OPTION_KEEP_VALUES.get();
                    }
                }
            }
        }

        /**
         * Generate an appropriate menu option which should be used in the case where a property can be reset to its
         * default behavior.
         */
        private <T> LocalizableMessage getResetToDefaultValuesMenuOption(PropertyDefinition<T> pd,
                SortedSet<T> defaultValues, SortedSet<T> currentValues) {
            DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd);
            boolean isMandatory = pd.hasOption(PropertyOption.MANDATORY);

            if (!isMandatory && query.isAlias()) {
                return INFO_EDITOR_OPTION_RESET_DEFAULT_ALIAS.get(query.getAliasDescription());
            } else if (query.isDefined()) {
                // Only show this option if the current value is different
                // to the default.
                if (!currentValues.equals(defaultValues)) {
                    LocalizableMessage svalue = getPropertyValues(pd, defaultValues);
                    if (defaultValues.size() > 1) {
                        return INFO_EDITOR_OPTION_RESET_DEFAULT_VALUES.get(svalue);
                    } else {
                        return INFO_EDITOR_OPTION_RESET_DEFAULT_VALUE.get(svalue);
                    }
                } else {
                    return null;
                }
            } else if (!isMandatory && query.isInherited()) {
                if (defaultValues.isEmpty()) {
                    if (query.getAliasDescription() != null) {
                        return INFO_EDITOR_OPTION_RESET_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription());
                    } else {
                        return INFO_EDITOR_OPTION_RESET_DEFAULT_INHERITED_ALIAS_UNDEFINED.get();
                    }
                } else {
                    LocalizableMessage svalue = getPropertyValues(pd, defaultValues);
                    if (defaultValues.size() > 1) {
                        return INFO_EDITOR_OPTION_RESET_INHERITED_DEFAULT_VALUES.get(svalue);
                    } else {
                        return INFO_EDITOR_OPTION_RESET_INHERITED_DEFAULT_VALUE.get(svalue);
                    }
                }
            } else if (!isMandatory && query.isUndefined()) {
                return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get();
            } else {
                return null;
            }
        }

        /** Common menu processing. */
        private <T> MenuResult<Boolean> runMenu(final PropertyDefinition<T> d, ConsoleApplication app,
                final SortedSet<T> defaultValues, final SortedSet<T> oldValues, final SortedSet<T> currentValues,
                MenuCallback<Boolean> addCallback, MenuCallback<Boolean> removeCallback) {
            // Construct a menu of actions.
            MenuBuilder<Boolean> builder = new MenuBuilder<>(app);
            builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName()));

            // First option is for leaving the property unchanged or
            // applying changes, but only if the state of the property is valid.
            if (!d.hasOption(PropertyOption.MANDATORY) || !currentValues.isEmpty()) {
                MenuResult<Boolean> result;
                if (!oldValues.equals(currentValues)) {
                    result = MenuResult.success(true);
                } else {
                    result = MenuResult.cancel();
                }

                LocalizableMessage option = getKeepDefaultValuesMenuOption(d, defaultValues, oldValues, currentValues);
                builder.addNumberedOption(option, result);
                builder.setDefault(LocalizableMessage.raw("1"), result);
            }

            // Add an option for adding some values.
            if (addCallback != null) {
                int i = builder.addNumberedOption(INFO_EDITOR_OPTION_ADD_ONE_OR_MORE_VALUES.get(), addCallback);
                if (d.hasOption(PropertyOption.MANDATORY) && currentValues.isEmpty()) {
                    builder.setDefault(LocalizableMessage.raw("%d", i), addCallback);
                }
            }

            // Add options for removing values if applicable.
            if (!currentValues.isEmpty()) {
                builder.addNumberedOption(INFO_EDITOR_OPTION_REMOVE_ONE_OR_MORE_VALUES.get(), removeCallback);
            }

            // Add options for removing all values and for resetting the
            // property to its default behavior.
            LocalizableMessage resetOption = null;
            if (!currentValues.equals(defaultValues)) {
                resetOption = getResetToDefaultValuesMenuOption(d, defaultValues, currentValues);
            }

            if (!currentValues.isEmpty() && (resetOption == null || !defaultValues.isEmpty())) {
                MenuCallback<Boolean> callback = new MenuCallback<Boolean>() {
                    @Override
                    public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                        isLastChoiceReset = false;
                        currentValues.clear();
                        app.println();
                        app.pressReturnToContinue();
                        return MenuResult.success(false);
                    }
                };
                builder.addNumberedOption(INFO_EDITOR_OPTION_REMOVE_ALL_VALUES.get(), callback);
            }

            if (resetOption != null) {
                MenuCallback<Boolean> callback = new MenuCallback<Boolean>() {

                    @Override
                    public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                        currentValues.clear();
                        currentValues.addAll(defaultValues);
                        isLastChoiceReset = true;
                        app.println();
                        app.pressReturnToContinue();
                        return MenuResult.success(false);
                    }

                };

                builder.addNumberedOption(resetOption, callback);
            }

            // Add an option for undoing any changes.
            if (!oldValues.equals(currentValues)) {
                MenuCallback<Boolean> callback = new MenuCallback<Boolean>() {

                    @Override
                    public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                        currentValues.clear();
                        currentValues.addAll(oldValues);
                        isLastChoiceReset = false;
                        app.println();
                        app.pressReturnToContinue();
                        return MenuResult.success(false);
                    }

                };

                builder.addNumberedOption(INFO_EDITOR_OPTION_REVERT_CHANGES.get(), callback);
            }

            builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));
            builder.addQuitOption();

            Menu<Boolean> menu = builder.toMenu();
            MenuResult<Boolean> result;
            try {
                app.println();
                result = menu.run();
            } catch (ClientException e) {
                this.e = e;
                return null;
            }

            if (result.isSuccess()) {
                if (result.getValue()) {

                    // Set the new property value(s).
                    mo.setPropertyValues(d, currentValues);

                    registerModification(d, currentValues, oldValues);

                    app.println();
                    app.pressReturnToContinue();
                    return MenuResult.success(false);
                } else {
                    // Continue until cancel/apply changes.
                    app.println();
                    return MenuResult.again();
                }
            } else if (result.isCancel()) {
                app.println();
                app.pressReturnToContinue();
                return MenuResult.success(false);
            } else {
                return MenuResult.quit();
            }
        }
    }

    /** A help call-back which displays a description and summary of a single property. */
    private static final class PropertyHelpCallback implements HelpCallback {

        /** The managed object definition. */
        private final ManagedObjectDefinition<?, ?> d;

        /** The property to be edited. */
        private final PropertyDefinition<?> pd;

        /** Creates a new property helper for the specified property. */
        private PropertyHelpCallback(ManagedObjectDefinition<?, ?> d, PropertyDefinition<?> pd) {
            this.d = d;
            this.pd = pd;
        }

        @Override
        public void display(ConsoleApplication app) {
            app.println();
            HelpSubCommandHandler.displayVerboseSingleProperty(app, d, pd.getName());
            app.println();
            app.pressReturnToContinue();
        }
    }

    /** A menu call-back for viewing a read-only properties. */
    private final class ReadOnlyPropertyViewer extends PropertyDefinitionVisitor<MenuResult<Boolean>, Void> implements
            MenuCallback<Boolean> {

        /** Any exception that was caught during processing. */
        private ClientException e;

        /** The managed object being edited. */
        private final ManagedObject<?> mo;

        /** The property to be edited. */
        private final PropertyDefinition<?> pd;

        /** Creates a new property editor for the specified property. */
        private ReadOnlyPropertyViewer(ManagedObject<?> mo, PropertyDefinition<?> pd) {
            this.mo = mo;
            this.pd = pd;
        }

        @Override
        public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
            MenuResult<Boolean> result = pd.accept(this, null);
            if (e != null) {
                throw e;
            }
            return result;
        }

        @Override
        public <T> MenuResult<Boolean> visitUnknown(PropertyDefinition<T> pd, Void p) {
            SortedSet<T> values = mo.getPropertyValues(pd);

            app.println();
            app.println();
            switch (values.size()) {
            case 0:
                // Only alias, undefined, or inherited alias or undefined
                // properties should apply here.
                DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd);
                LocalizableMessage aliasDescription = query.getAliasDescription();
                if (aliasDescription != null) {
                    app.println(INFO_EDITOR_HEADING_READ_ONLY_ALIAS.get(pd.getName(), aliasDescription));
                } else {
                    app.println(INFO_EDITOR_HEADING_READ_ONLY_ALIAS_UNDEFINED.get(pd.getName()));
                }
                break;
            case 1:
                LocalizableMessage svalue = getPropertyValues(pd, mo);
                app.println(INFO_EDITOR_HEADING_READ_ONLY_VALUE.get(pd.getName(), svalue));
                break;
            default:
                app.println(INFO_EDITOR_HEADING_READ_ONLY_VALUES.get(pd.getName()));
                app.println();
                displayPropertyValues(app, pd, values);
                break;
            }

            app.println();
            boolean result;
            try {
                result = app.confirmAction(INFO_EDITOR_PROMPT_READ_ONLY.get(), false);
            } catch (ClientException e) {
                this.e = e;
                return null;
            }

            if (result) {
                app.println();
                HelpSubCommandHandler.displayVerboseSingleProperty(app, mo.getManagedObjectDefinition(), pd.getName());
                app.println();
                app.pressReturnToContinue();
            }

            return MenuResult.again();
        }
    }

    /** A menu call-back for editing a modifiable single-valued property. */
    private final class SingleValuedPropertyEditor extends PropertyDefinitionVisitor<MenuResult<Boolean>, Void>
            implements MenuCallback<Boolean> {

        /** Any exception that was caught during processing. */
        private ClientException e;

        /** The managed object being edited. */
        private final ManagedObject<?> mo;

        /** The property to be edited. */
        private final PropertyDefinition<?> pd;

        /** Creates a new property editor for the specified property. */
        private SingleValuedPropertyEditor(ManagedObject<?> mo, PropertyDefinition<?> pd) {
            Reject.ifFalse(!pd.hasOption(PropertyOption.MULTI_VALUED));

            this.mo = mo;
            this.pd = pd;
        }

        @Override
        public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
            displayPropertyHeader(app, pd);

            MenuResult<Boolean> result = pd.accept(this, null);
            if (e != null) {
                throw e;
            }
            return result;
        }

        @Override
        public <C extends ConfigurationClient, S extends Configuration> MenuResult<Boolean> visitAggregation(
                AggregationPropertyDefinition<C, S> d, Void p) {
            // Construct a menu of actions.
            MenuBuilder<String> builder = new MenuBuilder<>(app);
            builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD);
            builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName()));

            DefaultBehaviorQuery<String> query = DefaultBehaviorQuery.query(d);
            SortedSet<String> currentValues = mo.getPropertyValues(d);
            SortedSet<String> defaultValues = mo.getPropertyDefaultValues(d);
            String currentValue = currentValues.isEmpty() ? null : currentValues.first();
            String defaultValue = defaultValues.isEmpty() ? null : defaultValues.first();

            // First option is for leaving the property unchanged.
            LocalizableMessage option = getKeepDefaultValuesMenuOption(d);
            builder.addNumberedOption(option, MenuResult.<String> cancel());
            builder.setDefault(LocalizableMessage.raw("1"), MenuResult.<String> cancel());

            // Create a list of possible names.
            final Set<String> values = new TreeSet<>(d);
            ManagedObjectPath<?, ?> path = d.getParentPath();
            InstantiableRelationDefinition<C, S> rd = d.getRelationDefinition();
            try {
                values.addAll(Arrays.asList(context.listManagedObjects(path, rd)));
            } catch (AuthorizationException e) {
                this.e = new ClientException(
                        ReturnCode.CLIENT_SIDE_PARAM_ERROR, LocalizableMessage.raw(e.getMessage()));
                return MenuResult.quit();
            } catch (ManagedObjectNotFoundException e) {
                this.e = new ClientException(ReturnCode.NO_SUCH_OBJECT, e.getMessageObject());
                return MenuResult.cancel();
            } catch (LdapException e) {
                this.e = new ClientException(ReturnCode.APPLICATION_ERROR, LocalizableMessage.raw(e.getMessage()));
                return MenuResult.quit();
            }

            final LocalizableMessage ufn = rd.getUserFriendlyName();
            for (String value : values) {
                if (currentValue != null && d.compare(value, currentValue) == 0) {
                    // This option is unnecessary.
                    continue;
                }

                LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value));
                if (value.equals(defaultValue) && query.isDefined()) {
                    option = INFO_EDITOR_OPTION_CHANGE_TO_DEFAULT_COMPONENT.get(ufn, svalue);
                } else {
                    option = INFO_EDITOR_OPTION_CHANGE_TO_COMPONENT.get(ufn, svalue);
                }

                builder.addNumberedOption(option, MenuResult.success(value));
            }
            MenuCallback<String> callback = new CreateComponentCallback<>(d);
            builder.addNumberedOption(INFO_EDITOR_OPTION_CREATE_A_NEW_COMPONENT.get(ufn), callback);

            // Third option is to reset the value back to its default.
            if (mo.isPropertyPresent(d) && !query.isDefined()) {
                option = getResetToDefaultValuesMenuOption(d);
                if (option != null) {
                    builder.addNumberedOption(option, MenuResult.<String> success());
                }
            }

            return runMenu(d, builder);
        }

        @Override
        public MenuResult<Boolean> visitBoolean(BooleanPropertyDefinition d, Void p) {
            // Construct a menu of actions.
            MenuBuilder<Boolean> builder = new MenuBuilder<>(app);
            builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName()));

            DefaultBehaviorQuery<Boolean> query = DefaultBehaviorQuery.query(d);
            SortedSet<Boolean> currentValues = mo.getPropertyValues(d);
            SortedSet<Boolean> defaultValues = mo.getPropertyDefaultValues(d);

            Boolean currentValue = currentValues.isEmpty() ? null : currentValues.first();
            Boolean defaultValue = defaultValues.isEmpty() ? null : defaultValues.first();

            // First option is for leaving the property unchanged.
            LocalizableMessage option = getKeepDefaultValuesMenuOption(d);
            builder.addNumberedOption(option, MenuResult.<Boolean> cancel());
            builder.setDefault(LocalizableMessage.raw("1"), MenuResult.<Boolean> cancel());

            // The second (and possibly third) option is to always change
            // the property's value.
            if (!Boolean.TRUE.equals(currentValue)) {
                LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(true));

                if (Boolean.TRUE.equals(defaultValue)) {
                    option = INFO_EDITOR_OPTION_CHANGE_TO_DEFAULT_VALUE.get(svalue);
                } else {
                    option = INFO_EDITOR_OPTION_CHANGE_TO_VALUE.get(svalue);
                }

                builder.addNumberedOption(option, MenuResult.success(true));
            }

            if (!Boolean.FALSE.equals(currentValue)) {
                LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(false));

                if (Boolean.FALSE.equals(defaultValue)) {
                    option = INFO_EDITOR_OPTION_CHANGE_TO_DEFAULT_VALUE.get(svalue);
                } else {
                    option = INFO_EDITOR_OPTION_CHANGE_TO_VALUE.get(svalue);
                }

                builder.addNumberedOption(option, MenuResult.success(false));
            }

            // Final option is to reset the value back to its default.
            if (mo.isPropertyPresent(d) && !query.isDefined()) {
                option = getResetToDefaultValuesMenuOption(d);
                if (option != null) {
                    builder.addNumberedOption(option, MenuResult.<Boolean> success());
                }
            }

            return runMenu(d, builder);
        }

        @Override
        public <E extends Enum<E>> MenuResult<Boolean> visitEnum(EnumPropertyDefinition<E> d, Void p) {
            // Construct a menu of actions.
            MenuBuilder<E> builder = new MenuBuilder<>(app);
            builder.setMultipleColumnThreshold(MULTI_COLUMN_THRESHOLD);
            builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName()));

            DefaultBehaviorQuery<E> query = DefaultBehaviorQuery.query(d);
            SortedSet<E> currentValues = mo.getPropertyValues(d);
            SortedSet<E> defaultValues = mo.getPropertyDefaultValues(d);
            E currentValue = currentValues.isEmpty() ? null : currentValues.first();
            E defaultValue = defaultValues.isEmpty() ? null : defaultValues.first();

            // First option is for leaving the property unchanged.
            LocalizableMessage option = getKeepDefaultValuesMenuOption(d);
            builder.addNumberedOption(option, MenuResult.<E> cancel());
            builder.setDefault(LocalizableMessage.raw("1"), MenuResult.<E> cancel());

            // Create options for changing to other values.
            Set<E> values = new TreeSet<>(d);
            values.addAll(EnumSet.allOf(d.getEnumClass()));
            for (E value : values) {
                if (value.equals(currentValue) && query.isDefined()) {
                    // This option is unnecessary.
                    continue;
                }

                LocalizableMessage svalue = getPropertyValues(d, Collections.singleton(value));

                if (value.equals(defaultValue) && query.isDefined()) {
                    option = INFO_EDITOR_OPTION_CHANGE_TO_DEFAULT_VALUE.get(svalue);
                } else {
                    option = INFO_EDITOR_OPTION_CHANGE_TO_VALUE.get(svalue);
                }

                builder.addNumberedOption(option, MenuResult.success(value));
            }

            // Third option is to reset the value back to its default.
            if (mo.isPropertyPresent(d) && !query.isDefined()) {
                option = getResetToDefaultValuesMenuOption(d);
                if (option != null) {
                    builder.addNumberedOption(option, MenuResult.<E> success());
                }
            }

            return runMenu(d, builder);
        }

        @Override
        public <T> MenuResult<Boolean> visitUnknown(final PropertyDefinition<T> d, Void p) {
            app.println();
            displayPropertySyntax(app, d);

            // Construct a menu of actions.
            MenuBuilder<T> builder = new MenuBuilder<>(app);
            builder.setPrompt(INFO_EDITOR_PROMPT_MODIFY_MENU.get(d.getName()));

            // First option is for leaving the property unchanged.
            LocalizableMessage option = getKeepDefaultValuesMenuOption(d);
            builder.addNumberedOption(option, MenuResult.<T> cancel());
            builder.setDefault(LocalizableMessage.raw("1"), MenuResult.<T> cancel());

            // The second option is to always change the property's value.
            builder.addNumberedOption(INFO_EDITOR_OPTION_CHANGE_VALUE.get(), new MenuCallback<T>() {

                @Override
                public MenuResult<T> invoke(ConsoleApplication app) throws ClientException {
                    app.println();
                    Set<T> values = readPropertyValues(app, mo.getManagedObjectDefinition(), d);
                    return MenuResult.success(values);
                }

            });

            // Third option is to reset the value back to its default.
            if (mo.isPropertyPresent(d)) {
                option = getResetToDefaultValuesMenuOption(d);
                if (option != null) {
                    builder.addNumberedOption(option, MenuResult.<T> success());
                }
            }

            return runMenu(d, builder);
        }

        /**
         * Generate an appropriate menu option for a property which asks the user whether they want to keep the
         * property's current settings.
         */
        private <T> LocalizableMessage getKeepDefaultValuesMenuOption(PropertyDefinition<T> pd) {
            DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd);
            SortedSet<T> currentValues = mo.getPropertyValues(pd);
            SortedSet<T> defaultValues = mo.getPropertyDefaultValues(pd);

            if (query.isDefined() && currentValues.equals(defaultValues)) {
                LocalizableMessage svalue = getPropertyValues(pd, currentValues);
                return INFO_EDITOR_OPTION_KEEP_DEFAULT_VALUE.get(svalue);
            } else if (mo.isPropertyPresent(pd)) {
                LocalizableMessage svalue = getPropertyValues(pd, currentValues);
                return INFO_EDITOR_OPTION_KEEP_VALUE.get(svalue);
            } else if (query.isAlias()) {
                return INFO_EDITOR_OPTION_KEEP_DEFAULT_ALIAS.get(query.getAliasDescription());
            } else if (query.isInherited()) {
                if (defaultValues.isEmpty()) {
                    if (query.getAliasDescription() != null) {
                        return INFO_EDITOR_OPTION_KEEP_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription());
                    } else {
                        return INFO_EDITOR_OPTION_KEEP_DEFAULT_INHERITED_ALIAS_UNDEFINED.get();
                    }
                } else {
                    LocalizableMessage svalue = getPropertyValues(pd, defaultValues);
                    return INFO_EDITOR_OPTION_KEEP_INHERITED_DEFAULT_VALUE.get(svalue);
                }
            } else {
                return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get();
            }
        }

        /**
         * Generate an appropriate menu option which should be used in the case where a property can be reset to its
         * default behavior.
         */
        private <T> LocalizableMessage getResetToDefaultValuesMenuOption(PropertyDefinition<T> pd) {
            DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd);
            SortedSet<T> currentValues = mo.getPropertyValues(pd);
            SortedSet<T> defaultValues = mo.getPropertyDefaultValues(pd);

            boolean isMandatory = pd.hasOption(PropertyOption.MANDATORY);

            if (!isMandatory && query.isAlias()) {
                return INFO_EDITOR_OPTION_RESET_DEFAULT_ALIAS.get(query.getAliasDescription());
            } else if (query.isDefined()) {
                // Only show this option if the current value is different
                // to the default.
                if (!currentValues.equals(defaultValues)) {
                    LocalizableMessage svalue = getPropertyValues(pd, defaultValues);
                    return INFO_EDITOR_OPTION_RESET_DEFAULT_VALUE.get(svalue);
                }
                return null;
            } else if (!isMandatory && query.isInherited()) {
                if (defaultValues.isEmpty()) {
                    if (query.getAliasDescription() != null) {
                        return INFO_EDITOR_OPTION_RESET_DEFAULT_INHERITED_ALIAS.get(query.getAliasDescription());
                    } else {
                        return INFO_EDITOR_OPTION_RESET_DEFAULT_INHERITED_ALIAS_UNDEFINED.get();
                    }
                } else {
                    LocalizableMessage svalue = getPropertyValues(pd, defaultValues);
                    return INFO_EDITOR_OPTION_RESET_INHERITED_DEFAULT_VALUE.get(svalue);
                }
            } else if (!isMandatory && query.isUndefined()) {
                return INFO_EDITOR_OPTION_LEAVE_UNDEFINED.get();
            }
            return null;
        }

        /** Common menu processing. */
        private <T> MenuResult<Boolean> runMenu(final PropertyDefinition<T> d, MenuBuilder<T> builder) {
            builder.addHelpOption(new PropertyHelpCallback(mo.getManagedObjectDefinition(), d));
            builder.addQuitOption();

            Menu<T> menu = builder.toMenu();
            MenuResult<T> result;
            try {
                app.println();
                result = menu.run();
            } catch (ClientException e) {
                this.e = e;
                return null;
            }

            if (result.isSuccess()) {
                // Set the new property value(s).
                Collection<T> values = result.getValues();
                // Both newValues and oldValues sets need to use the PropertyDefinition
                // as their comparator. Constructing a TreeSet<T> directly with the
                // values collection will fail if the values are e.g. InetAddresses.
                SortedSet<T> newValues = new TreeSet<>(d);
                newValues.addAll(values);
                SortedSet<T> oldValues = new TreeSet<>(mo.getPropertyValues(d));
                mo.setPropertyValues(d, values);

                // If there are no values when we do a reset.
                isLastChoiceReset = values.isEmpty();
                registerModification(d, newValues, oldValues);
                app.println();
                app.pressReturnToContinue();
                return MenuResult.success(false);
            } else if (result.isCancel()) {
                app.println();
                app.pressReturnToContinue();
                return MenuResult.success(false);
            } else {
                return MenuResult.quit();
            }
        }
    }

    /** Display a title and a description of the property. */
    private static void displayPropertyHeader(ConsoleApplication app, PropertyDefinition<?> pd) {
        app.println();
        app.println();
        app.println(INFO_EDITOR_HEADING_CONFIGURE_PROPERTY.get(pd.getName()));
        app.println();
        app.println(pd.getSynopsis(), 4);
        if (pd.getDescription() != null) {
            app.println();
            app.println(pd.getDescription(), 4);
        }
    }

    /** Display a property's syntax. */
    private static <T> void displayPropertySyntax(ConsoleApplication app, PropertyDefinition<T> d) {
        PropertyDefinitionUsageBuilder b = new PropertyDefinitionUsageBuilder(true);

        TableBuilder builder = new TableBuilder();
        builder.startRow();
        builder.appendCell(INFO_EDITOR_HEADING_SYNTAX.get());
        builder.appendCell(b.getUsage(d));

        TextTablePrinter printer = new TextTablePrinter(app.getErrorStream());
        printer.setDisplayHeadings(false);
        printer.setIndentWidth(4);
        printer.setColumnWidth(1, 0);
        builder.print(printer);
    }

    /** Display a table of property values. */
    private static <T> void displayPropertyValues(ConsoleApplication app, PropertyDefinition<T> pd,
            Collection<T> values) {
        TableBuilder builder = new TableBuilder();
        PropertyValuePrinter valuePrinter = new PropertyValuePrinter(null, null, false);

        int sz = values.size();
        boolean useMultipleColumns = (sz >= MULTI_COLUMN_THRESHOLD);
        int rows = sz;
        if (useMultipleColumns) {
            // Display in two columns the first column should contain
            // half the values. If there are an odd number of columns
            // then the first column should contain an additional value
            // (e.g. if there are 23 values, the first column should
            // contain 12 values and the second column 11 values).
            rows /= 2;
            rows += sz % 2;
        }

        List<T> vl = new ArrayList<>(values);
        for (int i = 0, j = rows; i < rows; i++, j++) {
            builder.startRow();
            builder.appendCell("*)");
            builder.appendCell(valuePrinter.print(pd, vl.get(i)));

            if (useMultipleColumns && j < sz) {
                builder.appendCell();
                builder.appendCell("*)");
                builder.appendCell(valuePrinter.print(pd, vl.get(j)));
            }
        }

        TextTablePrinter printer = new TextTablePrinter(app.getErrorStream());
        printer.setDisplayHeadings(false);
        printer.setIndentWidth(4);
        printer.setColumnWidth(1, 0);
        if (useMultipleColumns) {
            printer.setColumnWidth(2, 2);
            printer.setColumnWidth(4, 0);
        }
        builder.print(printer);
    }

    /** Display the set of values associated with a property. */
    private static <T> LocalizableMessage getPropertyValues(PropertyDefinition<T> pd, Collection<T> values) {
        if (values.isEmpty()) {
            // There are no values or default values. Display the default
            // behavior for alias values.
            DefaultBehaviorQuery<T> query = DefaultBehaviorQuery.query(pd);
            LocalizableMessage content = query.getAliasDescription();
            if (content != null) {
                return content;
            }
            return LocalizableMessage.raw("-");
        } else {
            PropertyValuePrinter printer = new PropertyValuePrinter(null, null, false);
            LocalizableMessageBuilder builder = new LocalizableMessageBuilder();

            boolean isFirst = true;
            for (T value : values) {
                if (!isFirst) {
                    builder.append(", ");
                }
                builder.append(printer.print(pd, value));
                isFirst = false;
            }

            return builder.toMessage();
        }
    }

    /** Display the set of values associated with a property. */
    private static <T> LocalizableMessage getPropertyValues(PropertyDefinition<T> pd, ManagedObject<?> mo) {
        SortedSet<T> values = mo.getPropertyValues(pd);
        return getPropertyValues(pd, values);
    }

    /** Read new values for a property. */
    private static <T> SortedSet<T> readPropertyValues(ConsoleApplication app, ManagedObjectDefinition<?, ?> d,
            PropertyDefinition<T> pd) throws ClientException {
        SortedSet<T> values = new TreeSet<>(pd);
        readPropertyValues(app, d, pd, values);
        return values;
    }

    /** Add values to a property. */
    private static <T> void readPropertyValues(ConsoleApplication app, ManagedObjectDefinition<?, ?> d,
            PropertyDefinition<T> pd, SortedSet<T> values) throws ClientException {
        // Make sure there is at least one value if mandatory and empty.
        if (values.isEmpty()) {
            while (true) {
                try {
                    LocalizableMessage prompt;

                    if (pd.hasOption(PropertyOption.MANDATORY)) {
                        prompt = INFO_EDITOR_PROMPT_READ_FIRST_VALUE.get(pd.getName());
                    } else {
                        prompt = INFO_EDITOR_PROMPT_READ_FIRST_VALUE_OPTIONAL.get(pd.getName());
                    }

                    app.println();
                    String s = app.readLineOfInput(prompt);
                    if (s.trim().length() == 0 && !pd.hasOption(PropertyOption.MANDATORY)) {
                        return;
                    }

                    T value = pd.decodeValue(s);
                    if (values.contains(value)) {
                        // Prevent addition of duplicates.
                        app.println();
                        app.println(ERR_EDITOR_READ_FIRST_DUPLICATE.get(s));
                    } else {
                        values.add(value);
                    }

                    break;
                } catch (PropertyException e) {
                    app.errPrintln();
                    app.errPrintln(ArgumentExceptionFactory.adaptPropertyException(e, d).getMessageObject());
                }
            }
        }

        if (pd.hasOption(PropertyOption.MULTI_VALUED)) {
            // Prompt for more values if multi-valued.
            while (true) {
                try {
                    LocalizableMessage prompt = INFO_EDITOR_PROMPT_READ_NEXT_VALUE.get(pd.getName());

                    app.println();
                    String s = app.readLineOfInput(prompt);
                    if (s.trim().length() == 0) {
                        return;
                    }

                    T value = pd.decodeValue(s);
                    if (values.contains(value)) {
                        // Prevent addition of duplicates.
                        app.println();
                        app.println(ERR_EDITOR_READ_NEXT_DUPLICATE.get(s));
                    } else {
                        values.add(value);
                    }
                } catch (PropertyException e) {
                    app.errPrintln();
                    app.errPrintln(ArgumentExceptionFactory.adaptPropertyException(e, d).getMessageObject());
                    app.errPrintln();
                }
            }
        }
    }

    /** The threshold above which choice menus should be displayed in multiple columns. */
    private static final int MULTI_COLUMN_THRESHOLD = 8;

    /** The application console. */
    private final ConsoleApplication app;

    /** The management context. */
    private final ManagementContext context;

    /**
     * The modifications performed: we assume that at most there is one modification per property definition.
     */
    private final List<PropertyEditorModification<?>> mods = new ArrayList<>();

    /** Whether the last type of choice made by the user in a menu is a reset. */
    private boolean isLastChoiceReset;

    /**
     * Create a new property value editor which will read from the provided application console.
     *
     * @param app
     *            The application console.
     * @param context
     *            The management context.
     */
    public PropertyValueEditor(ConsoleApplication app, ManagementContext context) {
        this.app = app;
        this.context = context;
    }

    /**
     * Interactively edits the properties of a managed object. Only the properties listed in the provided collection
     * will be accessible to the client. It is up to the caller to ensure that the list of properties does not include
     * read-only, monitoring, hidden, or advanced properties as appropriate.
     *
     * @param mo
     *            The managed object.
     * @param c
     *            The collection of properties which can be edited.
     * @param isCreate
     *            Flag indicating whether the managed object is being created. If it is then read-only properties
     *            will be modifiable.
     * @return Returns {@code MenuResult.success()} if the changes made to the managed object should be applied, or
     *         {@code MenuResult.cancel()} if the user to chose to cancel any changes, or {@code MenuResult.quit()} if
     *         the user chose to quit the application.
     * @throws ClientException
     *             If the user input could not be retrieved for some reason.
     */
    public MenuResult<Void> edit(ManagedObject<?> mo, Collection<PropertyDefinition<?>> c, boolean isCreate)
            throws ClientException {

        // Get values for this missing mandatory property.
        for (PropertyDefinition<?> pd : c) {
            if (pd.hasOption(PropertyOption.MANDATORY) && mo.getPropertyValues(pd).isEmpty()) {
                MandatoryPropertyInitializer mpi = new MandatoryPropertyInitializer(mo, pd);
                MenuResult<Void> result = mpi.invoke(app);
                if (!result.isSuccess()) {
                    return result;
                }
            }
        }

        while (true) {
            // Construct the main menu.
            MenuBuilder<Boolean> builder = new MenuBuilder<>(app);

            String ufn = mo.getManagedObjectPath().getName();
            if (ufn == null) {
                ufn = mo.getManagedObjectDefinition().getUserFriendlyName().toString();
            }
            builder.setPrompt(INFO_EDITOR_HEADING_CONFIGURE_COMPONENT.get(ufn));

            LocalizableMessage heading1 = INFO_DSCFG_HEADING_PROPERTY_NAME.get();
            LocalizableMessage heading2 = INFO_DSCFG_HEADING_PROPERTY_VALUE.get();
            builder.setColumnHeadings(heading1, heading2);
            builder.setColumnWidths(null, 0);

            // Create an option for editing/viewing each property.
            for (PropertyDefinition<?> pd : c) {
                // Determine whether this property should be modifiable.
                boolean isReadOnly = pd.hasOption(PropertyOption.MONITORING);
                if (!isCreate && pd.hasOption(PropertyOption.READ_ONLY)) {
                    isReadOnly = true;
                }

                // Create the appropriate property action.
                MenuCallback<Boolean> callback;
                if (isReadOnly) {
                    callback = new ReadOnlyPropertyViewer(mo, pd);
                } else if (pd.hasOption(PropertyOption.MULTI_VALUED)) {
                    callback = new MultiValuedPropertyEditor(mo, pd);
                } else {
                    callback = new SingleValuedPropertyEditor(mo, pd);
                }

                // Create the numeric option.
                LocalizableMessage values = getPropertyValues(pd, mo);
                builder.addNumberedOption(LocalizableMessage.raw("%s", pd.getName()), callback, values);
            }

            // Add a help option which displays a summary of the managed
            // object's definition.
            HelpCallback helpCallback = new ComponentHelpCallback(mo, c);
            builder.addHelpOption(helpCallback);

            // Add an option to apply the changes.
            if (isCreate) {
                builder.addCharOption(INFO_EDITOR_OPTION_FINISH_KEY.get(),
                        INFO_EDITOR_OPTION_FINISH_CREATE_COMPONENT.get(ufn), MenuResult.success(true));
            } else {
                builder.addCharOption(INFO_EDITOR_OPTION_FINISH_KEY.get(),
                        INFO_EDITOR_OPTION_FINISH_MODIFY_COMPONENT.get(ufn), MenuResult.success(true));
            }

            builder.setDefault(INFO_EDITOR_OPTION_FINISH_KEY.get(), MenuResult.success(true));

            // Add options for canceling and quitting.
            if (app.isMenuDrivenMode()) {
                builder.addCancelOption(false);
            }
            builder.addQuitOption();

            // Run the menu - success indicates that any changes should be
            // committed.
            app.println();
            app.println();
            Menu<Boolean> menu = builder.toMenu();
            MenuResult<Boolean> result = menu.run();

            if (result.isSuccess()) {
                if (result.getValue()) {
                    return MenuResult.success();
                }
            } else if (result.isCancel()) {
                return MenuResult.cancel();
            } else {
                return MenuResult.quit();
            }
        }
    }

    /**
     * Register the modification in the list of modifications.
     *
     * @param <T>
     *            The type of the underlying property associated with the modification.
     * @param pd
     *            the property definition.
     * @param newValues
     *            the resulting values of the property once the modification is applied.
     * @param previousValues
     *            the values we had before the modification is applied (these are not necessarily the *original* values
     *            if we already have other modifications applied to the same property).
     */
    private <T> void registerModification(PropertyDefinition<T> pd, SortedSet<T> newValues,
            SortedSet<T> previousValues) {

        if (isLastChoiceReset) {
            registerResetModification(pd, previousValues);
        } else if (!newValues.equals(previousValues)) {
            if (newValues.containsAll(previousValues)) {
                if (newValues.size() <= 1) {
                    registerSetModification(pd, newValues, previousValues);
                } else {
                    registerAddModification(pd, newValues, previousValues);
                }
            } else if (previousValues.containsAll(newValues)) {
                registerRemoveModification(pd, newValues, previousValues);
            } else if (newValues.size() <= 1) {
                registerSetModification(pd, newValues, previousValues);
            } else {
                // Split into two operations: remove and add
                SortedSet<T> removedValues = new TreeSet<>(previousValues);
                removedValues.removeAll(newValues);

                PropertyEditorModification<T> removeMod = PropertyEditorModification.createRemoveModification(pd,
                        removedValues, previousValues);
                addModification(removeMod);

                SortedSet<T> retainedValues = new TreeSet<>(previousValues);
                retainedValues.retainAll(newValues);

                SortedSet<T> addedValues = new TreeSet<>(newValues);
                addedValues.removeAll(retainedValues);

                PropertyEditorModification<T> addMod = PropertyEditorModification.createAddModification(pd,
                        addedValues, retainedValues);
                addModification(addMod);
            }
        }
    }

    /**
     * Register a reset modification in the list of modifications.
     *
     * @param <T>
     *            The type of the underlying property associated with the modification.
     * @param pd
     *            the property definition.
     * @param previousValues
     *            the values we had before the modification is applied (these are not necessarily the *original* values
     *            if we already have other modifications applied to the same property).
     */
    private <T> void registerResetModification(PropertyDefinition<T> pd, SortedSet<T> previousValues) {
        PropertyEditorModification<?> mod = getModification(pd);
        SortedSet<T> originalValues;
        if (mod != null) {
            originalValues = new TreeSet<>(pd);
            castAndAddValues(originalValues, mod.getOriginalValues(), pd);
            removeModification(mod);
        } else {
            originalValues = new TreeSet<>(previousValues);
        }

        addModification(PropertyEditorModification.createResetModification(pd, originalValues));
    }

    /**
     * Register a set modification in the list of modifications.
     *
     * @param <T>
     *            The type of the underlying property associated with the modification.
     * @param pd
     *            the property definition.
     * @param newValues
     *            the resulting values of the property once the modification is applied.
     * @param previousValues
     *            the values we had before the modification is applied (these are not necessarily the *original* values
     *            if we already have other modifications applied to the same property).
     */
    private <T> void registerSetModification(PropertyDefinition<T> pd, SortedSet<T> newValues,
            SortedSet<T> previousValues) {
        PropertyEditorModification<?> mod = getModification(pd);
        SortedSet<T> originalValues;
        if (mod != null) {
            originalValues = new TreeSet<>(pd);
            castAndAddValues(originalValues, mod.getOriginalValues(), pd);
            removeModification(mod);
        } else {
            originalValues = new TreeSet<>(previousValues);
        }
        addModification(PropertyEditorModification.createSetModification(pd, newValues, originalValues));
    }

    /**
     * Register an add modification in the list of modifications.
     *
     * @param <T>
     *            The type of the underlying property associated with the modification.
     * @param pd
     *            the property definition.
     * @param newValues
     *            the resulting values of the property once the modification is applied.
     * @param previousValues
     *            the values we had before the modification is applied (these are not necessarily the *original* values
     *            if we already have other modifications applied to the same property).
     */
    private <T> void registerAddModification(PropertyDefinition<T> pd, SortedSet<T> newValues,
            SortedSet<T> previousValues) {
        PropertyEditorModification<?> mod = getModification(pd);
        PropertyEditorModification<T> newMod;
        SortedSet<T> originalValues;
        if (mod != null) {
            originalValues = new TreeSet<>(pd);
            castAndAddValues(originalValues, mod.getOriginalValues(), pd);
            if (mod.getType() == PropertyEditorModification.Type.ADD) {
                SortedSet<T> addedValues = new TreeSet<>(newValues);
                addedValues.removeAll(originalValues);
                newMod = PropertyEditorModification.createAddModification(pd, addedValues, originalValues);
            } else {
                newMod = PropertyEditorModification
                        .createSetModification(pd, new TreeSet<T>(newValues), originalValues);
            }
            removeModification(mod);
        } else {
            originalValues = new TreeSet<>(previousValues);
            SortedSet<T> addedValues = new TreeSet<>(newValues);
            addedValues.removeAll(originalValues);
            newMod = PropertyEditorModification.createAddModification(pd, addedValues, originalValues);
        }
        addModification(newMod);
    }

    /**
     * Register a remove modification in the list of modifications.
     *
     * @param <T>
     *            The type of the underlying property associated with the modification.
     * @param pd
     *            the property definition.
     * @param newValues
     *            the resulting values of the property once the modification is applied.
     * @param previousValues
     *            the values we had before the modification is applied (these are not necessarily the *original* values
     *            if we already have other modifications applied to the same property).
     */
    private <T> void registerRemoveModification(PropertyDefinition<T> pd, SortedSet<T> newValues,
            SortedSet<T> previousValues) {
        PropertyEditorModification<?> mod = getModification(pd);
        PropertyEditorModification<T> newMod;
        SortedSet<T> originalValues;
        if (mod != null) {
            originalValues = new TreeSet<>(pd);
            castAndAddValues(originalValues, mod.getOriginalValues(), pd);
            if (newValues.isEmpty()) {
                newMod = PropertyEditorModification.createRemoveModification(pd, originalValues, originalValues);
            } else if (mod.getType() == PropertyEditorModification.Type.REMOVE) {
                SortedSet<T> removedValues = new TreeSet<>(originalValues);
                removedValues.removeAll(newValues);
                newMod = PropertyEditorModification.createRemoveModification(pd, removedValues, originalValues);
            } else {
                newMod = PropertyEditorModification
                        .createSetModification(pd, new TreeSet<T>(newValues), originalValues);
            }
            removeModification(mod);
        } else {
            originalValues = new TreeSet<>(previousValues);
            SortedSet<T> removedValues = new TreeSet<>(originalValues);
            removedValues.removeAll(newValues);
            newMod = PropertyEditorModification.createRemoveModification(pd, removedValues, originalValues);
        }
        addModification(newMod);
    }

    /**
     * Returns the modifications that have been applied during the last call of the method PropertyValueEditor.edit.
     *
     * @return the modifications that have been applied during the last call of the method PropertyValueEditor.edit.
     */
    public Collection<PropertyEditorModification<?>> getModifications() {
        return mods;
    }

    /** Clears the list of modifications. */
    public void resetModifications() {
        mods.clear();
    }

    /**
     * Adds a modification to the list of modifications that have been performed.
     *
     * @param <T>
     *            The type of the underlying property associated with the modification.
     * @param mod
     *            the modification to be added.
     */
    private <T> void addModification(PropertyEditorModification<T> mod) {
        mods.add(mod);
    }

    /**
     * Removes a modification from the list of modifications that have been performed.
     *
     * @param <T>
     *            The type of the underlying property associated with the modification.
     * @param mod
     *            the modification to be removed.
     */
    private <T> boolean removeModification(PropertyEditorModification<T> mod) {
        return mods.remove(mod);
    }

    /**
     * Returns the modification associated with a given property definition: we assume that we have only one
     * modification per property definition (in the worst case we merge the modifications and generate a unique set
     * modification).
     *
     * @param <T>
     *            The type of the underlying property associated with the modification.
     * @param pd
     *            the property definition.
     * @return the modification associated with the provided property definition and <CODE>null</CODE> if no
     *         modification could be found.
     */
    private <T> PropertyEditorModification<?> getModification(PropertyDefinition<T> pd) {
        PropertyEditorModification<?> mod = null;

        for (PropertyEditorModification<?> m : mods) {
            if (pd.equals(m.getPropertyDefinition())) {
                mod = m;
                break;
            }
        }

        return mod;
    }

    /**
     * This method is required to avoid compilation warnings. It basically adds the contents of a collection to another
     * collection by explicitly casting its values. This is done because the method getModification() returns au
     * undefined type.
     *
     * @param <T>
     *            The type of the destination values.
     * @param destination
     *            the collection that we want to update.
     * @param source
     *            the collection whose values we want to add (and cast) to the source collection.
     * @param pd
     *            the PropertyDefinition we use to do the casting.
     * @throws ClassCastException
     *             if an error occurs during the cast of the objects.
     */
    private <T> void castAndAddValues(Collection<T> destination, Collection<?> source, PropertyDefinition<T> pd) {
        for (Object o : source) {
            destination.add(pd.castValue(o));
        }
    }
}
